using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace BAFHTTPD
{
    public struct RequestInfo
    {
        // Request Stuff
        public string Request;
        public HTTPMethod Method;
        public Dictionary<string, string> Headers;
        public Dictionary<string, string> GetData;
        public Dictionary<string, string> PostData;

        // Client Info
        public System.Net.EndPoint EndPoint;

        // whatever else is needed.
    }

    public enum HTTPMethod
    {
        OPTIONS,
        GET,
        HEAD,
        POST,
        PUT,
        DELETE,
        TRACE,
        CONNECT,
        UNKNOWN
    }

    public class BAFHTTPD
    {
        // Configuration stuff
        public int Port = 0;
        //string ErrorPageTemplate = "<html><head><title>{error}</title></head><body><h1>{error}</h1><p>{explain}</p><hr><address>{server}</address></body></html>";
        string ServerString = "BAFHTTPD/0.2 (BAFIRC)";

        // "File" handeling
        public delegate int FileHandler(RequestInfo Request, out string Output, out string ContentType);
        public Dictionary<string, FileHandler> Handlers = null;
        public Dictionary<string, string> Files = null;
        public Dictionary<string, string> Directories = null;

        // Misc stuff
        static char[] SpaceSplitDelim = { ' ' };

        // Main httpd stuff
        private bool Running = false;
        private Thread ServerThread = null;

        public BAFHTTPD()
        {
            this.Port = 80;
            Handlers = new Dictionary<string, FileHandler>();
            Files = new Dictionary<string, string>();
            Directories = new Dictionary<string, string>();
        }

        public BAFHTTPD(int Port)
        {
            this.Port = Port;
            Handlers = new Dictionary<string, FileHandler>();
            Files = new Dictionary<string, string>();
            Directories = new Dictionary<string, string>();
        }

        ~BAFHTTPD()
        {
            StopHttpD();
        }


        public void StartHttpD()
        {
            if (Running)
                return;

            ServerThread = new Thread(new ThreadStart(HttpDThread));
            ServerThread.Start();
        }

        public void StopHttpD()
        {
            Running = false;
        }


        public void HttpDThread()
        {
            ErrorPageProvider ErrorPage = new ErrorPageProvider();
            ErrorPage.ServerString = ServerString;

            Socket Listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP);
            try
            {
                Listener.Blocking = true;
                Listener.Bind(new IPEndPoint(IPAddress.Any, Port));
                Listener.Listen(10);

                Running = true;

                while (Running)
                {
                    // Get Connection and Request
                    Socket Client = Listener.Accept();
                    Client.Blocking = false;

                    HTTPMethod Method = HTTPMethod.UNKNOWN;
                    string RequestURI = "";
                    string HTTPVersion = "";
                    Dictionary<string, string> Headers = new Dictionary<string, string>();

                    byte[] ClientBuffer = new byte[2048];
                    string ClientRequest = "";
                    bool CompleteHeaders = false, InvalidRequest = false;
                    int Received = 0, ParseIndex = 0;
                    DateTime HeaderTimeOut = DateTime.Now.AddSeconds(30);
                    Console.WriteLine("New request");
                    while (HeaderTimeOut >= DateTime.Now && !CompleteHeaders && !InvalidRequest)
                    {
                        bool DidntBlock = false;
                        ClientBuffer.Initialize();
                        try
                        {
                            Received = Client.Receive(ClientBuffer, 2048, SocketFlags.None);
                        }
                        catch (SocketException se)
                        {
                            if (se.SocketErrorCode != SocketError.WouldBlock)
                                throw se;

                            DidntBlock = true;
                        }

                        if (Received > 0)
                            ClientRequest += Encoding.ASCII.GetString(ClientBuffer).Substring(0, Received);

                        if (!DidntBlock)
                        {
                            if (Received == 0) // Disconnection
                                break;
                        }

                        while (ClientRequest.IndexOf("\r\n", ParseIndex) != -1) // Parse everything we can!
                        {
                            if (ClientRequest.IndexOf("\r\n", ParseIndex) == ParseIndex)
                            {
                                CompleteHeaders = true;
                                break;
                            }

                            string This = ClientRequest.Substring(ParseIndex, ClientRequest.IndexOf("\r\n", ParseIndex) - ParseIndex);
                            bool IsFirstLine = ParseIndex == 0;
                            ParseIndex += This.Length + 2;

                            if (IsFirstLine)
                            {
                                string[] RequestLine = This.Split(SpaceSplitDelim);

                                if (RequestLine.Length != 3)
                                {
                                    InvalidRequest = true;
                                    break;
                                }

                                switch (RequestLine[0].ToUpper())
                                {
                                    case "OPTIONS":
                                        Method = HTTPMethod.OPTIONS;
                                        break;
                                    case "GET":
                                        Method = HTTPMethod.GET;
                                        break;
                                    case "HEAD":
                                        Method = HTTPMethod.HEAD;
                                        break;
                                    case "POST":
                                        Method = HTTPMethod.POST;
                                        break;
                                    case "PUT":
                                        Method = HTTPMethod.PUT;
                                        break;
                                    case "DELETE":
                                        Method = HTTPMethod.DELETE;
                                        break;
                                    case "TRACE":
                                        Method = HTTPMethod.TRACE;
                                        break;
                                    case "CONNECT":
                                        Method = HTTPMethod.CONNECT;
                                        break;
                                }

                                RequestURI = RequestLine[1];
                                HTTPVersion = RequestLine[2];
                            }
                            else
                            {
                                Console.WriteLine("CRASH? " + This);
                                if (This.IndexOf(':') > 0)
                                {
                                    string Header = This.Substring(0, This.IndexOf(':'));
                                    string Value = This.Substring(Header.Length + 2);
                                    if (!Headers.ContainsKey(Header) && Header.Length + Value.Length < 1024)
                                        Headers.Add(Header, Value);
                                }
                            }
                        }

                        if (DidntBlock)
                            System.Threading.Thread.Sleep(100);
                    }

                    // Check to see if there's data
                    string ContentLengthString = null;
                    int ContentLength = 0;
                    if (Headers.TryGetValue("Content-Length", out ContentLengthString))
                        ContentLength = int.Parse(ContentLengthString);

                    byte[] MessageBodyBuf = null;
                    if (ContentLength > 0)
                    {
                        Console.WriteLine("Message Body!");

                        MessageBodyBuf = new byte[ContentLength];
                        int Offset = 0;

                        // There is probably some content data buffered from the last recieve.
                        int LastChunkStart = ClientRequest.Length - Received;
                        Console.WriteLine("Data: {0} {1} {2}", ClientRequest.IndexOf("\r\n\r\n"), LastChunkStart, Received);
                        if ((ClientRequest.IndexOf("\r\n\r\n") - LastChunkStart) < (Received - 4)) // There's data left.
                        {
                            Console.WriteLine("Spare data");
//                            Console.Write(ClientRequest);
                            int DataStart = (ClientRequest.IndexOf("\r\n\r\n") + 4) - LastChunkStart;
                            int AmountData = Received - DataStart;
                            //int LastBufStart = ClientRequest.Length - Received;
                            //int BufDataStart = DataStart - LastBufStart;
                            //for (int i = BufDataStart; i < Received; ++i)
                                //MessageBodyBuf.SetValue(ClientBuffer.GetValue(i), i - BufDataStart);

                            for (int i = DataStart; i < DataStart + AmountData; ++i)
                                MessageBodyBuf.SetValue(ClientBuffer.GetValue(i), i - DataStart);

                            Offset = AmountData;
                            //Offset = 2048 - LastBufStart;
                        }

                        for (int i = Offset; i < ContentLength; )
                        {
                            Console.WriteLine(i);
                            i += Client.Receive(MessageBodyBuf, i, ContentLength, SocketFlags.None);
                        }
                        Console.WriteLine("Message Body: {0}", Encoding.ASCII.GetString(MessageBodyBuf));
                    }

                    Console.WriteLine("{0} <InvalidRequst={1}> : ", RequestURI, InvalidRequest);

                    RequestInfo RI = new RequestInfo();
                    RI.Method = Method;
                    RI.EndPoint = Client.RemoteEndPoint;
                    RI.Headers = Headers;
                    RI.Request = RequestURI;

                    RI.GetData = new Dictionary<string, string>();
                    RI.PostData = new Dictionary<string, string>();

                    string RequestedFile = RequestURI;
                    if (RequestedFile.IndexOf('?') > 0)
                    {
                        string GetVars = RequestedFile.Substring(RequestedFile.IndexOf('?') + 1);
                        RequestedFile = RequestedFile.Remove(RequestedFile.IndexOf('?'));

                        char[] AmpDelim = { '&' };
                        string[] GetVarsExploded = GetVars.Split(AmpDelim);

                        foreach (string ThisVar in GetVarsExploded)
                        {
                            string ThisVarName, ThisVarValue;
                            if (ThisVar.IndexOf('=') != -1)
                            {
                                ThisVarName = ThisVar.Substring(0, ThisVar.IndexOf('='));
                                ThisVarValue = ThisVar.Substring(ThisVar.IndexOf('=') + 1);
                            }
                            else
                            {
                                ThisVarName = ThisVar;
                                ThisVarValue = "";
                            }

                            RI.GetData.Add(ThisVarName, ThisVarValue);
                        }
                    }

                    if (Method == HTTPMethod.POST)
                    {
                        string ContentEncoding;
                        if(!Headers.TryGetValue("Content-Encoding", out ContentEncoding))
                            ContentEncoding = "iso-8859-1";

                        char[] AmpDelim = { '&' };
                        string[] PostVarsExploded = GetEncoder(ContentEncoding).GetString(MessageBodyBuf).Split(AmpDelim);

                        foreach (string ThisVar in PostVarsExploded)
                        {
                            string ThisVarName, ThisVarValue;
                            if (ThisVar.IndexOf('=') != -1)
                            {
                                ThisVarName = ThisVar.Substring(0, ThisVar.IndexOf('='));
                                ThisVarValue = ThisVar.Substring(ThisVar.IndexOf('=') + 1);
                            }
                            else
                            {
                                ThisVarName = ThisVar;
                                ThisVarValue = "";
                            }

                            RI.PostData.Add(ThisVarName, ThisVarValue);
                        }
                    }

                    bool DirectoryFound = false; // Check directories
                    string Directory = null;
                    string FileName = null;
                    string DirSoFar = RequestedFile.Substring(0, RequestedFile.IndexOf('/') + 1);
                    while(true)
                    {
                        Console.WriteLine(DirSoFar + " :: " + RequestedFile);
                        if(DirSoFar != "/")
                        {
                            if(Directories.TryGetValue(DirSoFar, out Directory))
                            {
                                DirectoryFound = true;
                                Console.WriteLine("found dir");
                                FileName = Directory + "\\" + RequestedFile.Substring(DirSoFar.Length);
                                Console.WriteLine("filename: " + FileName);
                                break;
                            }
                        }
                        if(RequestedFile.IndexOf('/', DirSoFar.Length) + 1 > 0)
                            DirSoFar = RequestedFile.Substring(0, RequestedFile.IndexOf('/', DirSoFar.Length) + 1);
                        else
                            break;
                    }

                    FileHandler Handler = null;
                    if (Method == HTTPMethod.UNKNOWN || InvalidRequest || Headers.Count > 100)
                    {
                        Handler = new FileHandler(ErrorPage.Do400);
                    }
                    else if (DirectoryFound && System.IO.File.Exists(FileName))
                    {
                        Handler = null;
                    }
                    else if (Files.TryGetValue(RequestedFile, out FileName) && System.IO.File.Exists(FileName))
                    {
                        Handler = null;
                    }
                    else if (Handlers.TryGetValue(RequestedFile, out Handler))
                    {
                    }
                    else
                    {
                        Handler = new FileHandler(ErrorPage.Do404);
                        //ErrorPage.Argument = RequestURI;
                    }
                    /*else
                    {
                        // 404 it.
                        Console.WriteLine("404'ing");
                        // Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
                        //string Page = ErrorPageTemplate.Replace("{error}", "404 Not Found").Replace("{server}", ServerString).Replace("{explain}", "The requested document was not found on this server.");
                        string Page = "";
                        ErrorPage.Argument = RequestURI;
                        ErrorPage.Do404(new RequestInfo(), out Page);
                        string Response =
                            "HTTP/1.1 404 Not Found\r\n" + 
                            "Date: " + DateTime.Now.ToString("r") + "\r\n" +
                            "Server: " + ServerString + "\r\n" +
                            "Content-Length: " + Page.Length + "\r\n" +
                            "Connection: close\r\n" +
                            "Content-Type: text/html; charset=iso-8859-1\r\n" +
                            "\r\n" + 
                            Page;
                        Client.Send(Encoding.UTF8.GetBytes(Response));
                        Client.Close();
                    }*/

                    ResponseBuilder Resp;

                    if (Handler != null)
                    {
                        string Page;
                        string ContentType;
                        int Response = Handler(RI, out Page, out ContentType);

                        /*string ResponseStr =
                            "HTTP/1.1 " + Response + "\r\n" +
                            "Date: " + DateTime.Now.ToString("r") + "\r\n" +
    >--                        "Server: " + ServerString + "\r\n" +
                            "Content-Length: " + Page.Length + "\r\n" +
    >--                        "Connection: close\r\n" +
                            "Content-Type: text/html; charset=iso-8859-1\r\n" +
                            "\r\n" +
                            Page;
                        Client.Send(Encoding.ASCII.GetBytes(ResponseStr));*/

                        Dictionary<string, string> AdHeaders = new Dictionary<string, string>();
                        AdHeaders.Add("Server", ServerString);
                        AdHeaders.Add("Connection", "close");
                        Resp = new ResponseBuilder(Response, Page, "iso-8559-1", ContentType, AdHeaders);
                    }
                    else
                    {
                        //string Requested = RI.Request.Substring(RI.Request.LastIndexOf('/') + 1);
                        //if (Requested.IndexOf('?') > 0)
                            //Requested = Requested.Remove(Requested.IndexOf('?'));

                        byte[] File = System.IO.File.ReadAllBytes(FileName);
                        Dictionary<string, string> AdHeaders = new Dictionary<string, string>();
                        AdHeaders.Add("Server", ServerString);
                        AdHeaders.Add("Connection", "close");

                        Winista.Mime.MimeTypes g_MimeTypes = new Winista.Mime.MimeTypes("mime-types.xml");
                        sbyte[] fileData = null;
                        using (System.IO.FileStream srcFile =
                            new System.IO.FileStream(FileName, System.IO.FileMode.Open))
                        {
                            byte[] data = new byte[srcFile.Length];
                            srcFile.Read(data, 0, (Int32)srcFile.Length);
                            fileData = Winista.Mime.SupportUtil.ToSByteArray(data);
                        }
                        Winista.Mime.MimeType oMimeType = g_MimeTypes.GetMimeType(fileData);
                        string MimeType = null;

                        if (oMimeType != null)
                            MimeType = oMimeType.Name;
                        else
                            MimeType = "application/octet-stream";

                        Console.WriteLine(MimeType);
                        Resp = new ResponseBuilder(200, File, "binary", MimeType, AdHeaders);
                    }

                    Client.Send(Resp.BuildResponse());
                    Client.Close();
                }

                Listener.Close();
                ServerThread.Abort();
            }
            catch (SocketException se)
            {
                Console.WriteLine(se.Message);
                return;
            }
        }

        private Encoding GetEncoder(string Name)
        {
            foreach (EncodingInfo ei in Encoding.GetEncodings())
            {
                Encoding e = ei.GetEncoding();

                if (e.BodyName == Name)
                    return e;
            }

            return Encoding.ASCII;
        }
    } 
}
